/******************************************************************************* 
 * Copyright (c) 2008 xored software, Inc.  
 * 
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Eclipse Public License v1.0 
 * which accompanies this distribution, and is available at 
 * http://www.eclipse.org/legal/epl-v10.html  
 * 
 * Contributors: 
 *     xored software, Inc. - initial API and Implementation (Yuri Strot) 
 *******************************************************************************/
package com.xored.glance.ui.controls.items;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Listener;

import com.xored.glance.ui.sources.ITextSourceListener;
import com.xored.glance.ui.utils.TextUtils;

/**
 * @author Yuri Strot
 * 
 */
public class ItemDecorator implements Listener {

	public static final int DEFAULT_STYLE = SWT.BACKGROUND | SWT.FOREGROUND
			| SWT.SELECTED | SWT.HOT;

	protected Composite composite;
	protected ItemProvider provider;
	protected int style;

	protected TextLayout textLayout;
	protected Map<ItemCell, StyleRange[]> itemToMatches;
	protected Map<ItemCell, StyleRange[]> cacheStyles;
	protected List<ItemCell> cells;
	protected HashSet<ItemCell> cellSet;

	private ListenerList listeners = new ListenerList();

	public ItemDecorator(Composite composite, ItemProvider provider) {
		this(composite, provider, DEFAULT_STYLE);
	}

	public ItemDecorator(Composite composite, ItemProvider provider, int style) {
		this.composite = composite;
		this.provider = provider;
		this.style = style;
		clearStyles();
		init();
	}

	public void addTextSourceListener(ITextSourceListener listener) {
		listeners.add(listener);
	}

	public void removeTextSourceListener(ITextSourceListener listener) {
		listeners.remove(listener);
	}

	public ITextSourceListener[] getListeners() {
		Object[] objects = listeners.getListeners();
		ITextSourceListener[] listeners = new ITextSourceListener[objects.length];
		System.arraycopy(objects, 0, listeners, 0, objects.length);
		return listeners;
	}

	public void blocksChanged(ItemCell[] removed, ItemCell[] added) {
		for (ItemCell cell : removed) {
			cells.remove(cell);
			cellSet.remove(cell);
		}
		for (ItemCell cell : added) {
			cells.add(cell);
			cellSet.add(cell);
			cell.getItem().addListener(SWT.Dispose, this);
		}
		for (ITextSourceListener listener : getListeners()) {
			listener.blocksChanged(removed, added);
		}
	}

	public void clearStyles() {
		itemToMatches = new HashMap<ItemCell, StyleRange[]>();
		cacheStyles = new HashMap<ItemCell, StyleRange[]>();
	}

	public List<ItemCell> getCells() {
		return cells;
	}

	public void setCells(List<ItemCell> cells) {
		this.cells = cells;
		for (ItemCell cell : cells) {
			cell.getItem().addListener(SWT.Dispose, this);
		}
		cellSet = new HashSet<ItemCell>(cells);
	}

	public void setStyles(ItemCell cell, StyleRange[] styles) {
		if (styles == null)
			itemToMatches.remove(cell);
		else
			itemToMatches.put(cell, styles);
		cacheStyles.put(cell, calculateStyles(cell));
	}

	public void redraw() {
		Rectangle rect = composite.getClientArea();
		composite.redraw(rect.x, rect.y, rect.width, rect.height, true);
	}

	public void redraw(ItemCell cell) {
		Rectangle rect = provider.getBounds(cell.getItem(), cell.getIndex());
		composite.redraw(rect.x, rect.y, rect.width, rect.height, true);
	}

	protected TextLayout getTextLayout() {
		if (textLayout == null) {
			int orientation = composite.getStyle()
					& (SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT);
			textLayout = new TextLayout(composite.getDisplay());
			textLayout.setOrientation(orientation);
		} else {
			textLayout.setText("");
		}
		return textLayout;
	}

	protected StyleRange[] getRanges(ItemCell cell) {
		StyleRange[] ranges = cacheStyles.get(cell);
		if (ranges == null) {
			ranges = calculateStyles(cell);
			cacheStyles.put(cell, ranges);
		}
		return ranges;
	}

	protected StyleRange[] calculateStyles(ItemCell cell) {
		StyleRange[] cellStyles = cell.getStyles();
		StyleRange[] matchStyles = itemToMatches.get(cell);
		if (matchStyles == null || matchStyles.length == 0)
			return cellStyles;
		if (cellStyles.length == 0)
			return matchStyles;
		Region region = new Region(0, cell.getLength());
		int size = cellStyles.length + matchStyles.length;
		TextPresentation presentation = new TextPresentation(region, size);
		presentation.replaceStyleRanges(cellStyles);
		presentation.mergeStyleRanges(matchStyles);
		return TextUtils.getStyles(presentation);
	}

	public void handleEvent(Event event) {
		switch (event.type) {
		case SWT.PaintItem:
			paint(event);
			break;
		case SWT.EraseItem:
			erase(event);
			break;
		case SWT.Dispose:
			if (event.widget instanceof Item) {
				ItemCell cell = new ItemCell((Item) event.widget, event.index,
						provider);
				blocksChanged(new ItemCell[] { cell }, new ItemCell[0]);
			}
			break;
		}
	}

	protected void paint(Event event) {
		Item item = (Item) event.item;
		GC gc = event.gc;
		// remember colors to restore the GC later
		Color oldForeground = gc.getForeground();
		Color oldBackground = gc.getBackground();

		Color foreground = provider.getForeground(item, event.index);
		if (foreground != null) {
			gc.setForeground(foreground);
		}

		Color background = provider.getBackground(item, event.index);
		if (background != null) {
			gc.setBackground(background);
		}

		if ((event.detail & SWT.SELECTED) != 0) {
			Color color = new Color(null, 232, 242, 254);
			gc.setBackground(color);
		}

		if ((style & SWT.SELECTED) != 0 || (style & SWT.HOT) != 0) {
			gc.fillRectangle(provider.getBounds(item, event.index));
		}

		Image image = provider.getImage(item, event.index);
		if (image != null) {
			Rectangle imageBounds = provider.getImageBounds(item, event.index);
			if (imageBounds != null) {
				Rectangle bounds = image.getBounds();

				// center the image in the given space
				int x = imageBounds.x
						+ Math.max(0, (imageBounds.width - bounds.width) / 2);
				int y = imageBounds.y
						+ Math.max(0, (imageBounds.height - bounds.height) / 2);
				gc.drawImage(image, x, y);
			}
		}

		Rectangle textBounds = provider.getTextBounds(item, event.index);
		if (textBounds != null) {
			TextLayout layout = getTextLayout();
			layout.setText(provider.getText(item, event.index));
			layout.setFont(provider.getFont(item, event.index));

			ItemCell cell = new ItemCell(item, event.index, provider);
			if (!cellSet.contains(cell)) {
				blocksChanged(new ItemCell[0], new ItemCell[] { cell });
			}
			StyleRange[] ranges = getRanges(cell);
			for (StyleRange range : ranges) {
				layout.setStyle(range, range.start, range.start + range.length
						- 1);
			}

			Rectangle layoutBounds = layout.getBounds();

			int x = textBounds.x;
			int avg = (textBounds.height - layoutBounds.height) / 2;
			int y = textBounds.y + Math.max(0, avg);

			layout.draw(gc, x, y);
		}

		gc.setForeground(oldForeground);
		gc.setBackground(oldBackground);
	}

	public void erase(Event event) {
		event.detail &= ~style;
	}

	protected void init() {
		composite.addListener(SWT.EraseItem, this);
		composite.addListener(SWT.PaintItem, this);
		redraw();
	}

	public void dispose() {
		if (!disposed) {
			clearStyles();
			composite.removeListener(SWT.PaintItem, this);
			composite.removeListener(SWT.EraseItem, this);
			disposed = true;
			redraw();
		}
	}

	public boolean isDisposed() {
		return disposed;
	}

	private boolean disposed;

}
